Skip to content

Conversation

@j-piasecki
Copy link
Member

Summary

createAnimatedComponent is great, it handles a lot of things like events on the UI thread, CSS animations, binding animated props & styles to the component, etc. But everything comes at a cost, in this case, execution time. When building the new GestureDetector component, I noticed that a significant portion of the render time is spent on doing the stuff listed above, while the only thing the detector needs is support for events on the UI thread.

This PR adds NativeEventsManager to the public Reanimated API, which will then be used by react-native-gesture-handler to set up the UI thread event listeners.

Test plan

Try to import NativeEventsManager from react-native-reanimated.

j-piasecki added a commit to software-mansion/react-native-gesture-handler that referenced this pull request Dec 1, 2025
…tedComponent` when possible (#3837)

## Description

Shouldn't need
software-mansion/react-native-reanimated#8655 to
work properly

Instead of using `createAnimatedComponent`, which does a lot more stuff
other than setting up events, we can set up events ourselves using
Reanimated's `NativeEventsManager` directly.

The implementation in the PR should be independent of the Reanimated
version being used, as long as it's a relatively modern one.

## Test plan

<details>
<summary>Tested on this stress-test</summary>

```jsx
import React, { Profiler, useEffect, useRef } from 'react';
import { ScrollView, StyleSheet, View } from 'react-native';
import { GestureDetector, usePanGesture } from 'react-native-gesture-handler';
// import { PerfMonitor } from 'react-native-gesture-handler/src/v3/PerfMonitor';
import Animated, {
  useAnimatedStyle,
  useSharedValue,
} from 'react-native-reanimated';

const DATA = new Array(500).fill(null).map((_, i) => `Item ${i + 1}`);

function Item() {
  const translateX = useSharedValue(0);

  const style = useAnimatedStyle(() => {
    return {
      transform: [{ translateX: translateX.value }],
    };
  });

  const pan = usePanGesture({
    // disableReanimated: true,
    onUpdate: (event) => {
      'worklet';
      console.log('pan onUpdate', event.handlerData.changeX);
    },
    onStart: () => {
      'worklet';
      console.log('pan onStart', _WORKLET);
    },
    onEnd: () => {
      'worklet';
      console.log('pan onEnd');
    },
    onBegin: () => {
      'worklet';
      console.log('pan onBegin');
    },
    onFinalize: () => {
      'worklet';
      console.log('pan onFinalize');
    },
    onTouchesDown: () => {
      'worklet';
      console.log('pan onTouchesDown');
    },
  });

  return (
    <View style={{ height: 80, padding: 16, backgroundColor: 'gray' }}>
      <GestureDetector gesture={pan}>
        {/* <View collapsable={false} style={{opacity: 0.5}}> */}
        <Animated.View
          style={[
            { backgroundColor: 'red', height: '100%', aspectRatio: 1 },
            style,
          ]}
        />
        {/* </View> */}
      </GestureDetector>
    </View>
  );
}

function Benchmark() {
  return (
    <ScrollView
      style={{ flex: 1 }}
      contentContainerStyle={{ flexGrow: 1, gap: 8 }}>
      {DATA.map((_, index) => (
        <Item key={index} />
      ))}
    </ScrollView>
  );
}

const TIMES = 35;

export default function EmptyExample() {
  const times = useRef<number[]>([]).current;
  const [visible, setVisible] = React.useState(false);

  useEffect(() => {
    if (!visible && times.length < TIMES) {
      setTimeout(() => {
        setVisible(true);
      }, 24);
    }

    if (times.length === TIMES) {
      // calculate average, but remove highest and lowest
      const sortedTimes = [...times].sort((a, b) => a - b);
      sortedTimes.shift();
      sortedTimes.shift();
      sortedTimes.pop();
      sortedTimes.pop();
      const avgTime =
        sortedTimes.reduce((sum, time) => sum + time, 0) / sortedTimes.length;

      console.log(`Average render time: ${avgTime} ms`);

      // console.log(JSON.stringify(PerfMonitor.getMeasures(), null, 2));
      // PerfMonitor.clear();
    }
  }, [visible]);

  return (
    <View style={styles.container}>
      {visible && (
        <Profiler
          id="v3"
          onRender={(_id, _phase, actualDuration) => {
            times.push(actualDuration);
            setTimeout(() => {
              setVisible(false);
            }, 24);
          }}>
          <Benchmark />
        </Profiler>
      )}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
});

```
</details>

Before: 614ms, after: 576ms

---------

Co-authored-by: Michał Bert <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants